package com.qlogic.commons.utils.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.zip.CRC32;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

public final class JarUtil {

	private final static boolean IS_WINDOWS = (File.separatorChar == '\\');

	/**
	 * Writes all files of the given directory to the specified jar-file.
	 * 
	 * @param sourceDir
	 *            The directory containing the "source" files.
	 * @param target
	 *            The jar file which should be created
	 * @param compress
	 *            True when the jar file should be compressed
	 * @throws FileNotFoundException
	 *             when a file could not be found
	 * @throws IOException
	 *             when a file could not be read or the jar file could not be
	 *             written to.
	 */
	public static void jar(File sourceDir, File target, boolean compress)
			throws IOException {
		jar(sourceDir, target, compress);
	}

	/**
	 * Writes all given files to the specified jar-file.
	 * 
	 * @param files
	 *            all files that should be added to the JAR file
	 * @param sourceDir
	 *            The parent directory containing the given files.
	 * @param target
	 *            The jar file which should be created
	 * @param compress
	 *            True when the jar file should be compressed
	 * @throws FileNotFoundException
	 *             when a file could not be found
	 * @throws IOException
	 *             when a file could not be read or the jar file could not be
	 *             written to.
	 */
	public static void jar(File sourceDir, OutputStream target,
			boolean compress) throws IOException {
		File[] files = sourceDir.listFiles ();
		// creates target-jar-file:
		JarOutputStream out = new JarOutputStream(target);
		if (compress) {
			out.setLevel(ZipOutputStream.DEFLATED);
		} else {
			out.setLevel(ZipOutputStream.STORED);
		}
		// create a CRC32 object:
		CRC32 crc = new CRC32();
		byte[] buffer = new byte[1024 * 1024];
		// add all files:
		int sourceDirLength = sourceDir.getAbsolutePath().length() + 1;
		for (File file : files) {
			addFile(file, out, crc, sourceDirLength, buffer);
		}
		out.close();
	}

	/**
	 * Adds one file to the given jar file. If the specified file is a
	 * directory, all included files will be added.
	 * 
	 * @param file
	 *            The file which should be added
	 * @param out
	 *            The jar file to which the given jar file should be added
	 * @param crc
	 *            A helper class for the CRC32 calculation
	 * @param sourceDirLength
	 *            The number of chars which can be skipped from the file's path
	 * @param buffer
	 *            A buffer for reading the files.
	 * @throws FileNotFoundException
	 *             when the file was not found
	 * @throws IOException
	 *             when the file could not be read or not be added
	 */
	private static void addFile(File file, JarOutputStream out, CRC32 crc,
			int sourceDirLength, byte[] buffer) throws FileNotFoundException,
			IOException {
		if (file.isDirectory()) {
			File[] fileNames = file.listFiles();
			for (int i = 0; i < fileNames.length; i++) {
				addFile(fileNames[i], out, crc, sourceDirLength, buffer);
			}
		} else {
			String entryName = file.getAbsolutePath().substring(sourceDirLength);
			if (IS_WINDOWS) {
				entryName = entryName.replace('\\', '/');
			}
			JarEntry entry = new JarEntry(entryName);
			// read file:
			FileInputStream in = new FileInputStream(file);
			add(entry, in, out, crc, buffer);
		}
	}

	/**
	 * @param entry
	 * @param in
	 * @param out
	 * @param crc
	 * @param buffer
	 * @throws IOException
	 */
	private static void add(JarEntry entry, InputStream in,
			JarOutputStream out, CRC32 crc, byte[] buffer) throws IOException {
		out.putNextEntry(entry);
		int read;
		long size = 0;
		while ((read = in.read(buffer)) != -1) {
			crc.update(buffer, 0, read);
			out.write(buffer, 0, read);
			size += read;
		}
		entry.setCrc(crc.getValue());
		entry.setSize(size);
		in.close();
		out.closeEntry();
		crc.reset();
	}

	/**
	 * Adds the given file to the specified JAR file.
	 * 
	 * @param file
	 *            the file that should be added
	 * @param jarFile
	 *            The JAR to which the file should be added
	 * @param parentDir
	 *            the parent directory of the file, this is used to calculate
	 *            the path witin the JAR file. When null is given, the file will
	 *            be added into the root of the JAR.
	 * @param compress
	 *            True when the jar file should be compressed
	 * @throws FileNotFoundException
	 *             when the jarFile does not exist
	 * @throws IOException
	 *             when a file could not be written or the jar-file could not
	 *             read.
	 */
	public static void addToJar(File file, File jarFile, File parentDir,
			boolean compress) throws FileNotFoundException, IOException {
		File tmpJarFile = File.createTempFile("tmp", ".jar", jarFile
				.getParentFile());
		JarOutputStream out = new JarOutputStream(new FileOutputStream(
				tmpJarFile));
		if (compress) {
			out.setLevel(ZipOutputStream.DEFLATED);
		} else {
			out.setLevel(ZipOutputStream.STORED);
		}
		// copy contents of old jar to new jar:
		JarFile inputFile = new JarFile(jarFile);
		JarInputStream in = new JarInputStream(new FileInputStream(jarFile));
		CRC32 crc = new CRC32();
		byte[] buffer = new byte[512 * 1024];
		JarEntry entry = (JarEntry) in.getNextEntry();
		while (entry != null) {
			InputStream entryIn = inputFile.getInputStream(entry);
			add(entry, entryIn, out, crc, buffer);
			entryIn.close();
			entry = (JarEntry) in.getNextEntry();
		}
		in.close();
		inputFile.close();

		int sourceDirLength;
		if (parentDir == null) {
			sourceDirLength = file.getAbsolutePath().lastIndexOf(
					File.separatorChar) + 1;
		} else {
			sourceDirLength = file.getAbsolutePath().lastIndexOf(
					File.separatorChar)
					+ 1 - parentDir.getAbsolutePath().length();
		}
		addFile(file, out, crc, sourceDirLength, buffer);
		out.close();

		// remove old jar file and rename temp file to old one:
		if (jarFile.delete()) {
			if (!tmpJarFile.renameTo(jarFile)) {
				throw new IOException(
						"Unable to rename temporary JAR file to ["
								+ jarFile.getAbsolutePath() + "].");
			}
		} else {
			throw new IOException("Unable to delete old JAR file ["
					+ jarFile.getAbsolutePath() + "].");
		}

	}

	/**
	 * Extracts the given jar-file to the specified directory. The target
	 * directory will be cleaned before the jar-file will be extracted.
	 * 
	 * @param jarFile
	 *            The jar file which should be unpacked
	 * @param targetDir
	 *            The directory to which the jar-content should be extracted.
	 * @throws FileNotFoundException
	 *             when the jarFile does not exist
	 * @throws IOException
	 *             when a file could not be written or the jar-file could not
	 *             read.
	 */
	public static void unjar(File jarFile, File targetDir)
			throws FileNotFoundException, IOException {
		// clear target directory:
		if (targetDir.exists()) {
			targetDir.delete();
		}
		// create new target directory:
		targetDir.mkdirs();
		// read jar-file:
		String targetPath = targetDir.getAbsolutePath() + File.separatorChar;
		byte[] buffer = new byte[1024 * 1024];
		JarFile input = new JarFile(jarFile, false, ZipFile.OPEN_READ);
		Enumeration<JarEntry> enumeration = input.entries();
		for (; enumeration.hasMoreElements();) {
			JarEntry entry = enumeration.nextElement();
			if (!entry.isDirectory()) {
				// do not copy anything from the package cache:
				if (entry.getName().indexOf("package cache") == -1) {
					String path = targetPath + entry.getName();
					File file = new File(path);
					if (!file.getParentFile().exists()) {
						file.getParentFile().mkdirs();
					}
					FileOutputStream out = new FileOutputStream(file);
					InputStream in = input.getInputStream(entry);
					int read;
					while ((read = in.read(buffer)) != -1) {
						out.write(buffer, 0, read);
					}
					in.close();
					out.close();
				}
			}
		}

	}

	/**
	 * Extracts the given resource from a jar-file to the specified directory.
	 * 
	 * @param jarFile
	 *            The jar file which should be unpacked
	 * @param resource
	 *            The name of a resource in the jar
	 * @param targetDir
	 *            The directory to which the jar-content should be extracted.
	 * @throws FileNotFoundException
	 *             when the jarFile does not exist
	 * @throws IOException
	 *             when a file could not be written or the jar-file could not
	 *             read.
	 */
	public static void unjar(File jarFile, String resource, File targetDir)
			throws FileNotFoundException, IOException {
		// clear target directory:
		if (targetDir.exists()) {
			targetDir.delete();
		}
		// create new target directory:
		targetDir.mkdirs();
		// read jar-file:
		String targetPath = targetDir.getAbsolutePath() + File.separatorChar;
		byte[] buffer = new byte[1024 * 1024];
		JarFile input = new JarFile(jarFile, false, ZipFile.OPEN_READ);
		Enumeration<JarEntry> enumeration = input.entries();
		for (; enumeration.hasMoreElements();) {
			JarEntry entry = enumeration.nextElement();
			if (!entry.isDirectory()) {
				// do not copy anything from the package cache:
				if (entry.getName().equals(resource)) {
					String path = targetPath + entry.getName();
					File file = new File(path);
					if (!file.getParentFile().exists()) {
						file.getParentFile().mkdirs();
					}
					FileOutputStream out = new FileOutputStream(file);
					InputStream in = input.getInputStream(entry);
					int read;
					while ((read = in.read(buffer)) != -1) {
						out.write(buffer, 0, read);
					}
					in.close();
					out.close();
				}
			}
		}
	}

	/**
	 * Reads the package-names from the given jar-file.
	 * 
	 * @param jarFile
	 *            the jar file
	 * @return an array with all found package-names
	 * @throws IOException
	 *             when the jar-file could not be read
	 */
	public static String[] getPackageNames(File jarFile) throws IOException {
		HashMap<String, String> packageNames = new HashMap<String, String> ();
		JarFile input = new JarFile(jarFile, false, ZipFile.OPEN_READ);
		Enumeration<JarEntry> enumeration = input.entries();
		for (; enumeration.hasMoreElements();) {
			JarEntry entry = enumeration.nextElement();
			String name = entry.getName();
			if (name.endsWith(".class")) {
				int endPos = name.lastIndexOf('/');
				boolean isWindows = false;
				if (endPos == -1) {
					endPos = name.lastIndexOf('\\');
					isWindows = true;
				}
				name = name.substring(0, endPos);
				name = name.replace('/', '.');
				if (isWindows) {
					name = name.replace('\\', '.');
				}
				packageNames.put(name, name);
			}
		}
		return (String[]) packageNames.values().toArray(
				new String[packageNames.size()]);
	}

}
